{% extends 'base.attached.class' %}

{% block content %}
var {{ class_name }} = function(X, y, k, n, power) {

    this.X = X;
    this.y = y;
    this.k = k;
    this.n = n;
    this.power = power;

    var Neighbor = function(y, dist) {
        this.y = y;
        this.dist = dist;
    };

    var findMax = function(nums) {
        var i = 0, l = nums.length, idx = 0;
        for (; i < l; i++) {
            idx = nums[i] > nums[idx] ? i : idx;
        }
        return idx;
    };

    var compute = function(temp, cand, q) {  // minkowski distance
        var dist = 0., diff;
        for (var i = 0, l = temp.length; i < l; i++) {
            diff = Math.abs(temp[i] - cand[i]);
            if (q === 1) {
                dist += diff;
            } else if (q === 2) {
                dist += diff*diff;
            } else if (q === Number.POSITIVE_INFINITY) {
                if (diff > dist) {
                    dist = diff;
                }
            } else {
                dist += Math.pow(diff, q);
            }
        }
        if (q === 1 || q === Number.POSITIVE_INFINITY) {
            return dist;
        } else if (q === 2) {
            return Math.sqrt(dist);
        } else {
            return Math.pow(dist, 1. / q);
        }
    };

    this.predict = function(features) {
        var classProbas = this.predictProba(features);
        return findMax(classProbas);
    };

    this.predictProba = function(features) {
        var classProbas = new Array(this.n).fill(0);
        var i, dist;
        if (this.k === 1) {
            var classIdx = 0,
                minDist = Number.POSITIVE_INFINITY;
            for (i = 0; i < this.y.length; i++) {
                dist = compute(this.X[i], features, this.power);
                if (dist <= minDist) {
                    minDist = dist;
                    classIdx = this.y[i];
                }
            }
            classProbas[classIdx] = 1;
        } else {
            var dists = [];
            for (i = 0; i < this.y.length; i++) {
                dist = compute(this.X[i], features, this.power);
                dists.push(new Neighbor(this.y[i], dist));
            }
            dists.sort(function compare(n1, n2) {
                return (n1.dist < n2.dist) ? -1 : 1;
            });
            for (i = 0; i < this.k; i++) {
                classProbas[dists[i].y] += 1;
            }
            for (i = 0; i < this.n; i++) {
                classProbas[i] /= this.k;
            }
        }
        return classProbas;
    };

};

var main = function () {
    if (typeof process !== 'undefined' && typeof process.argv !== 'undefined') {
        if (process.argv.length - 2 !== {{ n_features }}) {
            var IllegalArgumentException = function(message) {
                this.message = message;
                this.name = "IllegalArgumentException";
            }
            throw new IllegalArgumentException("You have to pass {{ n_features }} features.");
        }
    }

    // Features:
    var features = process.argv.slice(2);
    for (var i = 0; i < features.length; i++) {
        features[i] = parseFloat(features[i]);
    }

    // Model data:
    {{ X }}
    {{ y }}

    // Estimator:
    var clf = new {{ class_name }}(X, y, {{ k }}, {{ n }}, {{ power }});

    {% if is_test or to_json %}
    // Get JSON:
    console.log(JSON.stringify({
        "predict": clf.predict(features),
        "predict_proba": clf.predictProba(features)
    }));
    {% else %}
    // Get class prediction:
    var prediction = clf.predict(features);
    console.log("Predicted class: #" + prediction);

    // Get class probabilities:
    var probabilities = clf.predictProba(features);
    for (var i = 0; i < probabilities.length; i++) {
        console.log("Probability of class #" + i + " : " + probabilities[i]);
    }
    {% endif %}

}

if (require.main === module) {
    main();
}
{% endblock %}